//! Black-box integration tests for the `ccc` binary's CLI subcommands //! (create/add/archive). Each test runs the real compiled binary as a //! subprocess against its own throwaway SQLite file, so no in-process //! state is shared between tests. use std::io::Write; use std::path::PathBuf; use std::process::Command; fn bin() -> &'static str { env!("CARGO_BIN_EXE_CCC") } struct TestEnv { db_path: PathBuf, } impl TestEnv { fn new(tag: &str) -> Self { let db_path = std::env::temp_dir().join(format!( "ccc-cli-test-{tag}-{}-{:?}.db", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); let _ = std::fs::remove_file(&db_path); Self { db_path } } fn cmd(&self, args: &[&str]) -> std::process::Output { Command::new(bin()) .args(args) .env("CCC_DB_PATH", &self.db_path) .output() .expect("failed to run ccc binary") } } impl Drop for TestEnv { fn drop(&mut self) { let _ = std::fs::remove_file(&self.db_path); } } fn write_temp_file(name: &str, contents: &[u8]) -> PathBuf { // Keep `name` (with its extension) as the trailing path component so // the CLI's mime-guessing logic sees the real file extension. let dir = std::env::temp_dir().join(format!( "ccc-cli-src-{}-{:?}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join(name); let mut f = std::fs::File::create(&path).unwrap(); f.write_all(contents).unwrap(); path } #[test] fn no_args_prints_usage_and_errors() { let out = Command::new(bin()) .args(["bogus-subcommand"]) .output() .expect("failed to run ccc binary"); assert!(!out.status.success()); let stderr = String::from_utf8_lossy(&out.stderr); assert!(stderr.contains("unknown subcommand"), "stderr was: {stderr}"); } #[test] fn create_package_succeeds() { let env = TestEnv::new("create-ok"); let out = env.cmd(&["create", "demo"]); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); let stdout = String::from_utf8_lossy(&out.stdout); assert!(stdout.contains("created package 'demo'"), "stdout was: {stdout}"); } #[test] fn create_duplicate_package_fails() { let env = TestEnv::new("create-dup"); let first = env.cmd(&["create", "demo"]); assert!(first.status.success()); let second = env.cmd(&["create", "demo"]); assert!(!second.status.success()); let stderr = String::from_utf8_lossy(&second.stderr); assert!(stderr.contains("already exists"), "stderr was: {stderr}"); } #[test] fn add_to_missing_package_fails() { let env = TestEnv::new("add-missing"); let src = write_temp_file("app.js", b"console.log(1)"); let out = env.cmd(&["add", "demo", src.to_str().unwrap(), "1.0.0"]); assert!(!out.status.success()); let stderr = String::from_utf8_lossy(&out.stderr); assert!(stderr.contains("does not exist"), "stderr was: {stderr}"); let _ = std::fs::remove_file(&src); } #[test] fn add_missing_source_file_fails() { let env = TestEnv::new("add-nofile"); assert!(env.cmd(&["create", "demo"]).status.success()); let out = env.cmd(&["add", "demo", "/nonexistent/path/does-not-exist.js", "1.0.0"]); assert!(!out.status.success()); } #[test] fn full_lifecycle_create_add_archive() { let env = TestEnv::new("lifecycle"); let src = write_temp_file("app.js", b"console.log('hello world');"); assert!(env.cmd(&["create", "demo"]).status.success()); let add_out = env.cmd(&["add", "demo", src.to_str().unwrap(), "1.0.0"]); assert!(add_out.status.success(), "stderr: {}", String::from_utf8_lossy(&add_out.stderr)); let archive_version = env.cmd(&["archive", "demo", "1.0.0"]); assert!(archive_version.status.success()); let archive_package = env.cmd(&["archive", "demo"]); assert!(archive_package.status.success()); let archive_again = env.cmd(&["archive", "demo", "9.9.9"]); assert!(!archive_again.status.success(), "archiving an unknown version should fail"); let _ = std::fs::remove_file(&src); } #[test] fn stats_on_empty_db_reports_zeros() { let env = TestEnv::new("stats-empty"); let out = env.cmd(&["stats"]); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); let stdout = String::from_utf8_lossy(&out.stdout); assert!(stdout.contains("0 active, 0 archived"), "stdout was: {stdout}"); assert!(stdout.contains("0 bytes (gzipped)"), "stdout was: {stdout}"); } #[test] fn stats_reflects_added_content() { let env = TestEnv::new("stats-content"); let src = write_temp_file("app.js", b"console.log('hello');"); assert!(env.cmd(&["create", "demo"]).status.success()); assert!(env.cmd(&["add", "demo", src.to_str().unwrap(), "1.0.0"]).status.success()); let out = env.cmd(&["stats"]); assert!(out.status.success()); let stdout = String::from_utf8_lossy(&out.stdout); assert!(stdout.contains("1 active, 0 archived"), "stdout was: {stdout}"); assert!(!stdout.contains("0 bytes (gzipped)"), "stdout was: {stdout}"); let _ = std::fs::remove_file(&src); } #[test] fn missing_required_args_exit_with_usage_code() { let env = TestEnv::new("usage"); let out = env.cmd(&["create"]); assert_eq!(out.status.code(), Some(2)); let out = env.cmd(&["add", "demo"]); assert_eq!(out.status.code(), Some(2)); }