use std::fs;
use std::path::PathBuf;
use std::process;
use hopper_schema::ProgramManifest;
pub fn cmd_test_gen(args: &[String]) {
if args.first().map(String::as_str) != Some("security") {
usage_and_exit();
}
let mut program_arg = None;
let mut out_path = PathBuf::from("tests/hopper_security_matrix.rs");
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--program" => {
i += 1;
if i >= args.len() {
usage_and_exit();
}
program_arg = Some(args[i].clone());
}
"--out" => {
i += 1;
if i >= args.len() {
usage_and_exit();
}
out_path = PathBuf::from(&args[i]);
}
"--help" | "-h" => usage_and_exit(),
other => {
eprintln!("Unknown test-gen argument: {other}");
usage_and_exit();
}
}
i += 1;
}
let program_arg = program_arg.unwrap_or_else(|| usage_and_exit());
let manifest = crate::load_program_manifest(&program_arg);
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent).unwrap_or_else(|err| {
eprintln!("Failed to create {}: {err}", parent.display());
process::exit(1);
});
}
fs::write(&out_path, security_matrix(&manifest)).unwrap_or_else(|err| {
eprintln!("Failed to write {}: {err}", out_path.display());
process::exit(1);
});
println!(
"Generated security test matrix for {} at {}",
manifest.name,
out_path.display()
);
}
fn usage_and_exit() -> ! {
eprintln!("Usage: hopper test-gen security --program <manifest> [--out tests/hopper_security_matrix.rs]");
process::exit(1);
}
fn security_matrix(manifest: &ProgramManifest) -> String {
let mut out = String::new();
out.push_str("//! Generated by `hopper test-gen security`.\n");
out.push_str("//! Fill the fixture builders with your local program-test harness.\n\n");
out.push_str("#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n");
out.push_str("enum SecurityCase { MissingSigner, WrongOwner, WrongPda, WrongLayout, NonWritableMut, WrongTokenExtension }\n\n");
out.push_str("#[test]\nfn generated_security_matrix_is_not_empty() {\n");
out.push_str(" let cases = security_cases();\n assert!(!cases.is_empty());\n}\n\n");
out.push_str("fn security_cases() -> &'static [(&'static str, SecurityCase)] {\n");
out.push_str(" &[\n");
for ix in manifest.instructions {
let base = sanitize(ix.name);
out.push_str(&format!(
" (\"{base}:missing-signer\", SecurityCase::MissingSigner),\n"
));
out.push_str(&format!(
" (\"{base}:wrong-owner\", SecurityCase::WrongOwner),\n"
));
out.push_str(&format!(
" (\"{base}:wrong-pda\", SecurityCase::WrongPda),\n"
));
out.push_str(&format!(
" (\"{base}:wrong-layout\", SecurityCase::WrongLayout),\n"
));
out.push_str(&format!(
" (\"{base}:non-writable-mut\", SecurityCase::NonWritableMut),\n"
));
if ix
.capabilities
.iter()
.any(|cap| cap.contains("token") || cap.contains("Token"))
{
out.push_str(&format!(
" (\"{base}:wrong-token-extension\", SecurityCase::WrongTokenExtension),\n"
));
}
}
out.push_str(" ]\n}\n");
out
}
fn sanitize(value: &str) -> String {
value
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '_' {
ch
} else {
'-'
}
})
.collect()
}