use std::process::{Command, Output}; fn command(arguments: &[&str], database_url: Option<&str>) -> Output { let mut command = Command::new(env!("CARGO_BIN_EXE_crank-migrate")); command.args(arguments); command.current_dir(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")); for (name, _) in std::env::vars() { if name.starts_with("CRANK_") || name.starts_with("POSTGRES_") || name.starts_with("OTEL_") { command.env_remove(name); } } if let Some(database_url) = database_url { command.env("CRANK_DATABASE_URL", database_url); } command.output().expect("migration command must run") } #[test] fn plan_is_deterministic_and_committed_contract_is_current() { let first = command(&["plan"], None); let second = command(&["plan"], None); assert!( first.status.success(), "{}", String::from_utf8_lossy(&first.stderr) ); assert_eq!(first.stdout, second.stdout); let plan: serde_json::Value = serde_json::from_slice(&first.stdout).unwrap(); assert_eq!(plan["sequence"].as_array().unwrap().len(), 3); let checked = command(&["plan", "--check"], None); assert!( checked.status.success(), "{}", String::from_utf8_lossy(&checked.stderr) ); } #[test] fn invalid_command_is_bounded_and_does_not_echo_arguments() { let canary = "secret-command-canary"; let output = command(&[canary], None); assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); assert!(stderr.len() < 1_024); assert!(!stderr.contains(canary)); let diagnostic: serde_json::Value = serde_json::from_str(stderr.trim()).unwrap(); assert_eq!(diagnostic["code"], "invalid_command"); } #[tokio::test] async fn database_only_config_can_apply_and_preflight_a_fresh_schema() { let database_url = crank_test_support::postgres_schema_url("test_migration_command").await; let applied = command(&["apply"], Some(&database_url)); assert!( applied.status.success(), "{}", String::from_utf8_lossy(&applied.stderr) ); let result: serde_json::Value = serde_json::from_slice(&applied.stdout).unwrap(); assert_eq!(result["status"], "applied"); let preflight = command(&["preflight"], Some(&database_url)); assert!( preflight.status.success(), "{}", String::from_utf8_lossy(&preflight.stderr) ); let result: serde_json::Value = serde_json::from_slice(&preflight.stdout).unwrap(); assert_eq!(result["status"], "current"); assert_eq!(result["version"], 3); } #[tokio::test] async fn migration_error_json_preserves_affected_version() { let database_url = crank_test_support::postgres_schema_url("test_migration_cli_version").await; assert!(command(&["apply"], Some(&database_url)).status.success()); let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); sqlx::query("update __crank_migrations set checksum = 'tampered' where version = 2") .execute(&pool) .await .unwrap(); let output = command(&["preflight"], Some(&database_url)); assert!(!output.status.success()); let diagnostic: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap(); assert_eq!(diagnostic["code"], "checksum_mismatch"); assert_eq!(diagnostic["version"], 2); }