use std::fs;
mod cli_runner;
#[ test ]
fn materialize_renders_templates_with_parameters()
{
let temp_dir = std::env::temp_dir();
let source_dir = temp_dir.join( "test_materialize_source" );
let archive_path = temp_dir.join( "test_materialize_archive.json" );
let destination = temp_dir.join( "test_materialize_output" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = fs::remove_dir_all( &destination );
fs::create_dir_all( &source_dir ).expect( "Should create source dir" );
fs::write(
source_dir.join( "readme.md" ),
"# {{project_name}}\n\nCreated by {{author}}"
).expect( "Should write template file" );
fs::write(
source_dir.join( "config.toml" ),
"name = \"{{project_name}}\"\nversion = \"{{version}}\""
).expect( "Should write config template" );
let script = format!(
".pack input::{} output::{}\n\
.archive.load path::{}\n\
.parameter.add name::project_name mandatory::1\n\
.parameter.add name::author mandatory::1\n\
.parameter.add name::version mandatory::0\n\
.value.set name::project_name value::\"my-project\"\n\
.value.set name::author value::\"Test User\"\n\
.value.set name::version value::\"1.0.0\"\n\
.materialize destination::{}\n\
exit",
source_dir.display(),
archive_path.display(),
archive_path.display(),
destination.display()
);
let output = cli_runner::repl_command( &script )
.output()
.expect( "Materialize workflow should execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!( output.status.success(), "Workflow should succeed. stdout: {stdout}, stderr: {stderr}" );
assert!( stdout.contains( "Materialized" ) || stdout.contains( "Created" ), "Should show success message" );
let readme_path = destination.join( "readme.md" );
let config_path = destination.join( "config.toml" );
assert!( readme_path.exists(), "readme.md should be created" );
assert!( config_path.exists(), "config.toml should be created" );
let readme_content = fs::read_to_string( &readme_path ).expect( "Should read readme" );
let config_content = fs::read_to_string( &config_path ).expect( "Should read config" );
assert!( readme_content.contains( "my-project" ), "Should substitute project_name in readme" );
assert!( readme_content.contains( "Test User" ), "Should substitute author in readme" );
assert!( !readme_content.contains( "{{" ), "Should not contain unreplaced variables in readme" );
assert!( config_content.contains( "my-project" ), "Should substitute project_name in config" );
assert!( config_content.contains( "1.0.0" ), "Should substitute version in config" );
assert!( !config_content.contains( "{{" ), "Should not contain unreplaced variables in config" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = fs::remove_dir_all( &destination );
}
#[ test ]
fn materialize_fails_without_mandatory_parameters()
{
let temp_dir = std::env::temp_dir();
let source_dir = temp_dir.join( "test_materialize_mandatory_source" );
let archive_path = temp_dir.join( "test_materialize_mandatory.json" );
let destination = temp_dir.join( "test_materialize_mandatory_output" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = fs::remove_dir_all( &destination );
fs::create_dir_all( &source_dir ).expect( "Should create source dir" );
fs::write( source_dir.join( "file.txt" ), "Value: {{mandatory_param}}" )
.expect( "Should write template" );
let script = format!(
".pack input::{} output::{}\n\
.archive.load path::{}\n\
.parameter.add name::mandatory_param mandatory::1\n\
.materialize destination::{}\n\
exit",
source_dir.display(),
archive_path.display(),
archive_path.display(),
destination.display()
);
let output = cli_runner::repl_command( &script )
.output()
.expect( "Command should execute" );
let combined = format!( "{}{}", String::from_utf8_lossy( &output.stdout ), String::from_utf8_lossy( &output.stderr ) );
assert!(
!output.status.success() || combined.contains( "ERROR" ) || combined.contains( "mandatory" ),
"Should fail or error for missing mandatory parameter. output: {combined}"
);
assert!( !destination.exists(), "Destination should not exist after failed materialize" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
}
#[ test ]
fn materialize_dry_run_preview()
{
let temp_dir = std::env::temp_dir();
let source_dir = temp_dir.join( "test_materialize_dry_source" );
let archive_path = temp_dir.join( "test_materialize_dry.json" );
let destination = temp_dir.join( "test_materialize_dry_output" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = fs::remove_dir_all( &destination );
fs::create_dir_all( &source_dir ).expect( "Should create source dir" );
fs::write( source_dir.join( "test.txt" ), "Hello {{name}}" )
.expect( "Should write template" );
let script = format!(
".pack input::{} output::{}\n\
.archive.load path::{}\n\
.value.set name::name value::\"World\"\n\
.materialize destination::{} dry::1\n\
exit",
source_dir.display(),
archive_path.display(),
archive_path.display(),
destination.display()
);
let output = cli_runner::repl_command( &script )
.output()
.expect( "Dry run should execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!( output.status.success(), "Dry run should succeed" );
assert!( stdout.contains( "Dry run" ) || stdout.contains( "Would" ), "Should indicate dry run mode" );
assert!( !destination.exists(), "Destination should NOT exist after dry run" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
}
#[ test ]
fn materialize_without_archive_returns_error()
{
let temp_dir = std::env::temp_dir();
let destination = temp_dir.join( "test_materialize_no_archive" );
let _ = fs::remove_dir_all( &destination );
let output = cli_runner::cargo_run_command( &[ ".materialize",
&format!( "destination::{}", destination.display() ),
] )
.output()
.expect( "Command should execute" );
assert!( !output.status.success(), "Should fail without loaded archive" );
let combined = format!(
"{}{}",
String::from_utf8_lossy( &output.stdout ),
String::from_utf8_lossy( &output.stderr )
);
assert!(
combined.contains( "No archive" ) || combined.contains( "ERROR" ) || combined.contains( "load" ),
"Should show clear error about missing archive"
);
let _ = fs::remove_dir_all( &destination );
}
#[ test ]
fn unpack_preserves_template_variables()
{
let temp_dir = std::env::temp_dir();
let source_dir = temp_dir.join( "test_unpack_source" );
let archive_path = temp_dir.join( "test_unpack_archive.json" );
let destination = temp_dir.join( "test_unpack_output" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = fs::remove_dir_all( &destination );
fs::create_dir_all( &source_dir ).expect( "Should create source dir" );
fs::write(
source_dir.join( "template.txt" ),
"Project: {{project_name}}\nAuthor: {{author}}"
).expect( "Should write template file" );
let script = format!(
".pack input::{} output::{}\n\
.archive.load path::{}\n\
.unpack destination::{}\n\
exit",
source_dir.display(),
archive_path.display(),
archive_path.display(),
destination.display()
);
let output = cli_runner::repl_command( &script )
.output()
.expect( "Unpack workflow should execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!( output.status.success(), "Workflow should succeed. stdout: {stdout}, stderr: {stderr}" );
assert!( stdout.contains( "Unpacked" ) || stdout.contains( "files" ), "Should show success message" );
let template_path = destination.join( "template.txt" );
assert!( template_path.exists(), "template.txt should be created" );
let content = fs::read_to_string( &template_path ).expect( "Should read unpacked file" );
assert!( content.contains( "{{project_name}}" ), "Should preserve {{project_name}} placeholder" );
assert!( content.contains( "{{author}}" ), "Should preserve {{author}} placeholder" );
assert!( !content.contains( "my-project" ), "Should NOT contain substituted values" );
assert!( !content.contains( "Test User" ), "Should NOT contain substituted values" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = fs::remove_dir_all( &destination );
}
#[ test ]
fn unpack_dry_run_preview()
{
let temp_dir = std::env::temp_dir();
let source_dir = temp_dir.join( "test_unpack_dry_source" );
let archive_path = temp_dir.join( "test_unpack_dry.json" );
let destination = temp_dir.join( "test_unpack_dry_output" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = fs::remove_dir_all( &destination );
fs::create_dir_all( &source_dir ).expect( "Should create source dir" );
fs::write( source_dir.join( "file.txt" ), "Hello {{name}}" )
.expect( "Should write template" );
let script = format!(
".pack input::{} output::{}\n\
.archive.load path::{}\n\
.unpack destination::{} dry::1\n\
exit",
source_dir.display(),
archive_path.display(),
archive_path.display(),
destination.display()
);
let output = cli_runner::repl_command( &script )
.output()
.expect( "Dry run should execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!( output.status.success(), "Dry run should succeed" );
assert!( stdout.contains( "Dry run" ) || stdout.contains( "Would" ), "Should indicate dry run mode" );
assert!( !destination.exists(), "Destination should NOT exist after dry run" );
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
}
#[ test ]
fn unpack_without_archive_returns_error()
{
let temp_dir = std::env::temp_dir();
let destination = temp_dir.join( "test_unpack_no_archive" );
let _ = fs::remove_dir_all( &destination );
let output = cli_runner::cargo_run_command( &[ ".unpack",
&format!( "destination::{}", destination.display() ),
] )
.output()
.expect( "Command should execute" );
assert!( !output.status.success(), "Should fail without loaded archive" );
let combined = format!(
"{}{}",
String::from_utf8_lossy( &output.stdout ),
String::from_utf8_lossy( &output.stderr )
);
assert!(
combined.contains( "No archive" ) || combined.contains( "ERROR" ) || combined.contains( "load" ),
"Should show clear error about missing archive"
);
let _ = fs::remove_dir_all( &destination );
}
#[ test ]
fn test_path_traversal_destination_rejected()
{
let temp_dir = std::env::temp_dir();
let source_dir = temp_dir.join( "test_traversal_source" );
let archive_path = temp_dir.join( "test_traversal_archive.json" );
let traversal_destination = "/tmp/traversal_safe/../../../etc/mat_traversal_test";
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
let _ = std::fs::remove_dir_all( "/etc/mat_traversal_test" );
fs::create_dir_all( &source_dir ).expect( "Should create source dir" );
fs::write( source_dir.join( "file.txt" ), "hello" ).expect( "Should write template" );
let script = format!(
".pack input::{} output::{}\n\
.archive.load path::{}\n\
.materialize destination::{}\n\
exit",
source_dir.display(),
archive_path.display(),
archive_path.display(),
traversal_destination
);
let output = cli_runner::repl_command( &script )
.output()
.expect( "Command should execute" );
assert!(
!output.status.success(),
"Path traversal in destination must cause failure. stdout: {}",
String::from_utf8_lossy( &output.stdout )
);
assert!(
!std::path::Path::new( "/etc/mat_traversal_test" ).exists(),
"Should not create directories under /etc via path traversal"
);
let _ = fs::remove_dir_all( &source_dir );
let _ = fs::remove_file( &archive_path );
}