use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
fn setup_test_dir() -> TempDir {
TempDir::new().expect("Failed to create temp dir")
}
fn forgedb_cmd(dir: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_forgedb"));
cmd.current_dir(dir);
cmd
}
fn create_test_schema(dir: &Path) {
let schema = r#"User {
id: +uuid
email: ^&string @email
created_at: +timestamp
}
"#;
fs::write(dir.join("schema.forge"), schema).expect("Failed to write schema");
}
#[test]
fn test_init_command_creates_project_structure() {
use forgedb::commands::init::{run, InitOptions};
let temp_dir = setup_test_dir();
let project_name = "test_project";
let project_path = temp_dir.path().join(project_name);
let options = InitOptions {
project_name: project_path.to_string_lossy().to_string(),
template: None,
rust: true,
api_only: false,
};
let result = run(options);
assert!(result.is_ok(), "Init command should succeed");
assert!(project_path.exists());
assert!(project_path.join("src").exists());
assert!(project_path.join("generated").exists());
assert!(project_path.join("data/db").exists());
assert!(project_path.join("schema.forge").exists());
assert!(project_path.join("forgedb.toml").exists());
assert!(project_path.join(".gitignore").exists());
assert!(project_path.join("README.md").exists());
assert!(project_path.join("Cargo.toml").exists());
assert!(project_path.join("src/main.rs").exists());
}
#[test]
fn test_init_with_blog_template() {
use forgedb::commands::init::{run, InitOptions};
let temp_dir = setup_test_dir();
let project_name = "blog_project";
let project_path = temp_dir.path().join(project_name);
let options = InitOptions {
project_name: project_path.to_string_lossy().to_string(),
template: Some("blog".to_string()),
rust: true,
api_only: false,
};
let result = run(options);
assert!(result.is_ok(), "Init with blog template should succeed");
let schema_content =
fs::read_to_string(project_path.join("schema.forge")).expect("Failed to read schema");
assert!(schema_content.contains("Post"));
assert!(schema_content.contains("Tag"));
}
#[test]
fn test_init_emits_onhost_systemd_deploy() {
use forgedb::commands::init::{run, InitOptions};
let temp_dir = setup_test_dir();
let project_name = "acme_svc";
let project_path = temp_dir.path().join(project_name);
let options = InitOptions {
project_name: project_path.to_string_lossy().to_string(),
template: None,
rust: true,
api_only: false,
};
run(options).expect("init should succeed");
assert!(project_path.join("Dockerfile").exists());
let deploy = project_path.join("deploy");
let service =
fs::read_to_string(deploy.join("acme_svc.service")).expect("systemd unit emitted");
let env = fs::read_to_string(deploy.join("acme_svc.env")).expect("EnvironmentFile emitted");
assert!(deploy.join("README.md").exists(), "install README emitted");
assert!(service.contains("ExecStart=/usr/local/bin/acme_svc"));
assert!(service.contains("EnvironmentFile=/etc/acme_svc/acme_svc.env"));
assert!(service.contains("Type=exec"), "readiness model is exec, not notify");
assert!(service.contains("DynamicUser=yes"), "non-root without a manual useradd");
assert!(service.contains("StateDirectory=acme_svc"), "managed data dir");
assert!(service.contains("KillSignal=SIGTERM"), "pairs with graceful shutdown");
assert!(service.contains("NoNewPrivileges=yes"), "hardening present");
assert!(service.contains("WantedBy=multi-user.target"));
assert!(env.contains("FORGEDB_DATA=/var/lib/acme_svc"), "env data dir agrees with StateDirectory");
assert!(env.contains("FORGEDB_HOST=0.0.0.0"));
assert!(!deploy.join("acme_svc@.service").exists(), "no instance-template unit scaffolded");
}
#[test]
fn test_generate_command_creates_rust_code() {
let temp_dir = setup_test_dir();
create_test_schema(temp_dir.path());
let output = forgedb_cmd(temp_dir.path())
.args(["generate", "rust", "--output", "generated", "--force"])
.output()
.expect("Failed to run forgedb generate");
assert!(
output.status.success(),
"Generate command should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let generated_file = temp_dir.path().join("generated/database.rs");
assert!(
generated_file.exists(),
"Generated database.rs should exist"
);
let generated_content =
fs::read_to_string(&generated_file).expect("Failed to read generated code");
assert!(generated_content.contains("User"));
assert!(generated_content.contains("email"));
}
#[test]
fn test_validate_command_detects_errors() {
let temp_dir = setup_test_dir();
let invalid_schema = r#"user {
id: +uuid
Email: string
}
"#;
fs::write(temp_dir.path().join("schema.forge"), invalid_schema)
.expect("Failed to write schema");
let output = forgedb_cmd(temp_dir.path())
.arg("validate")
.output()
.expect("Failed to run forgedb validate");
assert!(
!output.status.success(),
"Invalid schema should fail validation"
);
}
#[test]
fn test_validate_command_passes_valid_schema() {
let temp_dir = setup_test_dir();
create_test_schema(temp_dir.path());
let output = forgedb_cmd(temp_dir.path())
.args(["validate", "--schema-only"])
.output()
.expect("Failed to run forgedb validate");
assert!(
output.status.success(),
"Valid schema should pass validation: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_generate_all_is_idempotent_with_force() {
let temp_dir = setup_test_dir();
create_test_schema(temp_dir.path());
let first = forgedb_cmd(temp_dir.path())
.args(["generate", "all", "--output", "generated", "--force"])
.output()
.expect("Failed to run forgedb generate all (first run)");
assert!(
first.status.success(),
"First generate all --force should succeed: {}",
String::from_utf8_lossy(&first.stderr)
);
let second = forgedb_cmd(temp_dir.path())
.args(["generate", "all", "--output", "generated", "--force"])
.output()
.expect("Failed to run forgedb generate all (second run)");
assert!(
second.status.success(),
"Second generate all --force should succeed (idempotency — C1 fix): {}",
String::from_utf8_lossy(&second.stderr)
);
let third_no_force = forgedb_cmd(temp_dir.path())
.args(["generate", "all", "--output", "generated"])
.output()
.expect("Failed to run forgedb generate all (no-force third run)");
assert!(
!third_no_force.status.success(),
"generate all without --force should fail when files already exist"
);
}
#[test]
fn test_generate_check_mode() {
let temp_dir = setup_test_dir();
create_test_schema(temp_dir.path());
let output = forgedb_cmd(temp_dir.path())
.args(["generate", "rust", "--output", "generated", "--force"])
.output()
.expect("Failed to run forgedb generate");
assert!(
output.status.success(),
"Generate should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = forgedb_cmd(temp_dir.path())
.args(["generate", "rust", "--output", "generated", "--check"])
.output()
.expect("Failed to run forgedb generate --check");
assert!(
output.status.success(),
"Check mode should pass for up-to-date code: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_migrate_auto_records_and_scaffolds_authored_hop() {
let temp_dir = setup_test_dir();
let dir = temp_dir.path();
let v1 = "Widget {\n id: +uuid\n sku: &string\n qty: u32\n}\n";
let v2_additive = "Widget {\n id: +uuid\n sku: &string\n qty: u32\n note: string?\n}\n";
let v3_authored =
"Widget {\n id: +uuid\n sku: &string\n qty: string\n note: string?\n}\n";
fs::write(dir.join("v1.forge"), v1).unwrap();
fs::write(dir.join("v2.forge"), v2_additive).unwrap();
fs::write(dir.join("v3.forge"), v3_authored).unwrap();
let out = forgedb_cmd(dir)
.args(["migrate", "create", "baseline", "--auto", "--schema", "v1.forge"])
.output()
.expect("run migrate baseline");
assert!(out.status.success(), "baseline should succeed");
assert!(dir.join("migrations/.schema-snapshot.forge").exists(), "snapshot recorded");
assert!(dir.join("migrations/schemas/v1.forge").exists(), "baseline versioned schema recorded");
let out = forgedb_cmd(dir)
.args(["migrate", "create", "add_note", "--auto", "--schema", "v2.forge"])
.output()
.expect("run migrate additive");
assert!(out.status.success(), "additive migration should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("note"), "reports the added field: {stdout}");
assert!(
stdout.to_lowercase().contains("backfill"),
"additive prints the reopen fast-path advice: {stdout}"
);
assert!(dir.join("migrations/schemas/v2.forge").exists(), "v2 versioned schema recorded");
let out = forgedb_cmd(dir)
.args(["migrate", "create", "qty_to_string", "--auto", "--schema", "v3.forge"])
.output()
.expect("run migrate authored");
assert!(
out.status.success(),
"authored migration is recorded, not refused: {}",
String::from_utf8_lossy(&out.stderr)
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(combined.contains("[authored]"), "marks the authored hop: {combined}");
assert!(
combined.contains("migrate up"),
"prints the migrate up next steps: {combined}"
);
assert!(dir.join("migrations/schemas/v3.forge").exists(), "v3 versioned schema recorded");
let scaffolds: Vec<_> = fs::read_dir(dir.join("migrations"))
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_dir() && p.join("transform.rs").exists())
.collect();
assert_eq!(scaffolds.len(), 1, "exactly one authored-body scaffold written");
let body = fs::read_to_string(scaffolds[0].join("transform.rs")).unwrap();
assert!(
body.contains("authored_transform") && body.contains("TODO"),
"scaffold is the fill-in-the-TODO authored transform: {body}"
);
}